using System;

using System.IO;

namespace PTP.Core
{
  /// <summary>
  /// This exception is throw when there's error reading container
  /// </summary>
  public class InvalidContainerException : Exception
  {
    public InvalidContainerException(string msg) : base(msg) { }
  }

  public enum CommandCode
  {
    Undefined = 0x1000,
    GetDeviceInfo = 0x1001,
    OpenSession = 0x1002,
    CloseSession = 0x1003,
    GetStorageIDs = 0x1004,
    GetStorageInfo = 0x1005,
    GetNumObjects = 0x1006,
    GetObjectHandles = 0x1007,
    GetObjectInfo = 0x1008,
    GetObject = 0x1009,
    GetThumb = 0x100A,
    DeleteObject = 0x100B,
    SendObjectInfo = 0x100C,
    SendObject = 0x100D,
    InitiateCapture = 0x100E,
    FormatStore = 0x100F,
    ResetDevice = 0x1010,
    SelfTest = 0x1011,
    SetObjectProtection = 0x1012,
    PowerDown = 0x1013,
    GetDevicePropDesc = 0x1014,
    GetDevicePropValue = 0x1015,
    SetDevicePropValue = 0x1016,
    ResetDevicePropValue = 0x1017,
    TerminateOpenCapture = 0x1018,
    MoveObject = 0x1019,
    CopyObject = 0x101A,
    GetPartialObject = 0x101B,
    InitiateOpenCapture = 0x101C
  }

  public enum ContainerType
  {
    Undefined = 0,
    CommandBlock = 1,
    DataBlock = 2,
    ResponseBlock = 3,
    EventBlock = 4
  }

  /// <summary>
  /// Header of container
  /// 
  /// USB Still Image Capture Device Definition: The PIMA15740 information
  /// is encapsulated into the streams using containers. A container has four types;
  /// Command Block, Data Block, Response Block, and Event Block. The Event Blocks are
  /// not used on the Bulk-Pipe but rather the Interrupt Pipe for encapsulating
  /// asynchronous event data.
  /// </summary>
  public class ContainerHeader
  {
    /// <summary>
    /// Header length in bytes
    /// </summary>
    public const int HeaderLength = 12;

    /// <summary>
    /// Container Length (including payload length)
    /// </summary>
    public int Length;

    /// <summary>
    /// This field describes the type of the container
    /// </summary>
    public ContainerType ContainerType = ContainerType.Undefined;

    /// <summary>
    /// USB Still Image Capture Device Definition: This field contains the PIMA 15740 OperationCode, ResponseCode, or EventCode.
    /// </summary>
    public int Code;

    /// <summary>
    /// USB Still Image Capture Device Definition: This is a host generated number that associates all phases of an PIMA15740 operation
    /// </summary>
    public int TransactionID;

    public ContainerHeader() { }

    public ContainerHeader(Stream stream)
    {
      if (stream == null)
        throw new ArgumentNullException("stream");

      this.Length = readByte(stream) | (readByte(stream) << 8) | (readByte(stream) << 16) | (readByte(stream) << 24);
      this.ContainerType = (ContainerType)readByte(stream);
      //byte 5 skipped
      readByte(stream);
      this.Code = readByte(stream) | (readByte(stream) << 8);
      this.TransactionID = readByte(stream) | (readByte(stream) << 8) | (readByte(stream) << 16) | (readByte(stream) << 24);
    }

    private static int readByte(Stream s)
    {
      int result = s.ReadByte();
      if (result == -1)
        throw new InvalidContainerException("Unexpected end of stream");
      return result;
    }

    public void Write(Stream s)
    {
      s.WriteByte((byte)(0xff & this.Length));
      s.WriteByte((byte)(0xff & (this.Length >> 8)));
      s.WriteByte((byte)(0xff & (this.Length >> 16)));
      s.WriteByte((byte)(0xff & (this.Length >> 24)));
      s.WriteByte((byte)(0xff & (int)this.ContainerType));
      s.WriteByte(0);
      s.WriteByte((byte)(0xff & this.Code));
      s.WriteByte((byte)(0xff & (this.Code >> 8)));
      s.WriteByte((byte)(0xff & this.TransactionID));
      s.WriteByte((byte)(0xff & (this.TransactionID >> 8)));
      s.WriteByte((byte)(0xff & (this.TransactionID >> 16)));
      s.WriteByte((byte)(0xff & (this.TransactionID >> 24)));
    }

    public int PayloadLength
    {
      get
      {
        return this.Length - ContainerHeader.HeaderLength;
      }
      set
      {
        this.Length = ContainerHeader.HeaderLength + value;
      }
    }
  }

  public abstract class Container
  {
    public ContainerHeader Header;
    
    protected Container() { }

    protected Container(ContainerHeader header)
    {
      this.Header = header;
    }

    /// <summary>
    /// Write all bytes of this container to stream.
    /// </summary>
    /// <param name="s"></param>
    public void Write(Stream s)
    {
      this.Header.Write(s);
      this.WritePayload(s);
    }

    public abstract void WritePayload(Stream s);

  }

  public class DataBlockContainer : Container
  {
    public Stream Payload;

    public DataBlockContainer(ContainerHeader header, Stream payload)
    {
      this.Header = header;
      this.Payload = payload;
    }

    public override void WritePayload(Stream s)
    {
      throw new NotImplementedException();
    }
  }

  public class ParameterContainer : Container
  {
    public int[] Parameters;

    public ParameterContainer() { }

    public ParameterContainer(int[] parameters)
      : base()
    {
      this.Header = new ContainerHeader();
      this.Parameters = parameters;
      this.Header.PayloadLength = this.payloadLength;
    }

    protected int payloadLength
    {
      get { return (this.Parameters == null) ? 0 : this.Parameters.Length * 4; }
    }

    public override void WritePayload(Stream s)
    {
      if (payloadLength > 0)
      {
        for (int i = 0; i < this.Parameters.Length; i++)
        {
          s.WriteByte((byte)(0xff & this.Parameters[i]));
          s.WriteByte((byte)(0xff & (this.Parameters[i] >> 8)));
          s.WriteByte((byte)(0xff & (this.Parameters[i] >> 16)));
          s.WriteByte((byte)(0xff & (this.Parameters[i] >> 24)));
        }
      }
    }
  }

  public class CommandBlockContainer : ParameterContainer
  {
    public CommandBlockContainer(int commandCode, params int[] parameters)
      : base(parameters)
    {
      this.Header.Code = commandCode;
      this.Header.ContainerType = ContainerType.CommandBlock;
    }
  }

  public class ResponseBlockContainer : ParameterContainer
  {
    /// <summary>
    /// This constructor is used mainly for testing, because response blocks are
    /// usually read from stream
    /// </summary>
    /// <param name="responseCode"></param>
    /// <param name="parameters"></param>
    public ResponseBlockContainer(int responseCode, params int[] parameters)
      : base(parameters)
    {
      this.Header.Code = responseCode;
      this.Header.ContainerType = ContainerType.ResponseBlock;
    }

    /// <summary>
    /// Read response block from stream
    /// </summary>
    /// <param name="header">the container header</param>
    /// <param name="payload">data after header</param>
    public ResponseBlockContainer(ContainerHeader header, Stream payload)
    {
      if (payload == null)
        throw new ArgumentNullException("payload");

      this.Header = header;
      this.readParameters(payload);
    }

    protected void readParameters(Stream stream)
    {
      //PIMA 15740:2000: Response datasets may have at most five parameters
      //-> this operation doesn't use too much memory
      int pLength = this.Header.PayloadLength;
      if (pLength < 0)
        throw new InvalidContainerException("Invalid container length");
      if ((pLength % 4) != 0)
        throw new InvalidContainerException("Response payload length must by multiplier of 4");
      if (pLength > 20)
        throw new InvalidContainerException("Response datasets may have at most five parameters");

      int i = 0;
      this.Parameters = new int[pLength/4];
      while (i < this.Parameters.Length)
      {
        this.Parameters[i++] = stream.ReadByte() | (stream.ReadByte() << 8) | (stream.ReadByte() << 16) | (stream.ReadByte() << 24);
      }
     }
  }

  //ResponseCode Description
  //0x2000 Undefined
  //0x2001 OK
  //0x2002 General Error
  //0x2003 Session Not Open
  //0x2004 Invalid TransactionID
  //0x2005 Operation Not Supported
  //0x2006 Parameter Not Supported
  //0x2007 Incomplete Transfer
  //0x2008 Invalid StorageID
  //0x2009 Invalid ObjectHandle
  //0x200A DeviceProp Not Supported
  //0x200B Invalid ObjectFormatCode
  //0x200C Store Full
  //0x200D Object WriteProtected
  //0x200E Store Read-Only
  //0x200F Access Denied
  //0x2010 No Thumbnail Present
  //0x2011 SelfTest Failed
  //0x2012 Partial Deletion
  //0x2013 Store Not Available
  //0x2014 Specification By Format Unsupported
  //0x2015 No Valid ObjectInfo
  //0x2016 Invalid Code Format
  //0x2017 Unknown Vendor Code
  //0x2018 Capture Already Terminated
  //0x2019 Device Busy
  //0x201A Invalid ParentObject
  //0x201B Invalid DeviceProp Format
  //0x201C Invalid DeviceProp Value
  //0x201D Invalid Parameter
  //0x201E Session Already Open
  //0x201F Transaction Cancelled
  //0x2020 Specification of Destination Unsupported
  //All other codes
  //with MSN of 0010 Reserved
  //All codes with
  //MSN of 1010 Vendor-Extended Response Code

}
